#! /usr/bin/env python

"""
This implements a directory location and locking service to clients.
Client proxies can query the directory server for the file server
address of a particular pathname. They can also acquire and release
locks on particular files.

1. Directory location service

Client proxies can query the directory server for the file server
address of a particular pathname.  Clients invoke the lookup method with
a path name which the server will then attempt to map to a file
server location. The server will iterate in reverse over the given
pathname begining with the pathname itself and then all its directory
prefixes stopping if it finds a match or runs out of prefixes (i.e.
a match on the / prefix fails).

The mappings are stored in a simple configuration text file where lines
consit of the following format:

    pathname address:port

Anything prefixed with a '#' character is taken as a comment and ignored.

e.g.

     # pathname address:port
     /      localhost:10000
     /home  localhost:10001
     /var   localhost:10002


Sample Usage
============

Client calls:
   conn = DirSocket()

   conn.lookup(/home/user1/foo.txt)

Server returns:

    addr=(localhost,10000)
    path=/user1/foo.txt


2. Locking service

Client proxies can acquire and release lock on any file.
A client calls the trans_lock method on the server giving it the 
lock type, file identifier (fid) and transaction identifier (tid).

If the server find a conflicting lock already in place it will reject 
the lock request else it will update the lock state for the file and
and let the client known its been successful.

The client can use the same tid for different files allowing them
to lock multiple files as part of one transaction.
Clients can promote (read->write) or demote (write->read) lock types
if this doesn't cause a conflict with other lock holders.

To release a lock the client proxy calls the trans_unlock on the
server with just the tid which will release all the files locked
under that tid.

Note that the client must issue its own transaction ids.

2.1 lock_type : We support the following lock types:

1. read -  client want a shared lock for reads
2. write - client wants exclusive access for writes
3. watch - shared read/writes

2.2 file identifer (fid)

The unique fid returned from a file server.

2.3 transaction identifier tid.

The tid can be used by clients to lock 1 or more files.

Sample Usage
============
    conn = DirSocket()
    fid = proxy.lookup('/sample/foo.txt')
    tid = proxy.gen_tid()
    conn.trans_lock(LOCK_WRITE, tid, fid)
    ....
    conn.trans_unlock(tid)

3. Mainteance mode

The directory location mappings can be modified via rpc calls

We support the following operations:

get_diraddrs() - return list of current mappings
add_diraddr()  - add a new mapping
del_diraddr()  - remove a mapping

"""

import socket,threading
import string, errno, StringIO

from dfs_common import *

# globals
file_locks = {}
file_trans = {}

def release_trans(tid):
    lock_set = file_trans[tid]
    # release all locks for transaction
    for lock in lock_set:
       lock.update(LOCK_NONE, tid)
       if lock.get_count() == 0:
            del file_locks[lock.get_fid()]
    # clean up 
    lock_set.clear()
    del file_trans[tid]

class DirAddrMap(object):
    
    """This maintains directory to file server address mapping.
    
    Config file is assume to have the following format:
        directory host:port
    e.g.
        /sample localhost:10000

    This states that the directory /sample is hosted at addres localhost:10000
    """
    def __init__(self, config_fname):

        self.config_fname = config_fname
        self.dir_addrs = {}

        self.reset()

    def __str__(self):
        return str(self.dir_addrs)

    def lookup(self, dname):
        return self.dir_addrs.get(dname, None)

    def dump(self):
        return self.dir_addrs

    def add(self, dname, fsaddr):
        self.dir_addrs[dname] = fsaddr
        self.sync()

    def remove(self, dname):
        if dname in self.dir_addrs:
            del dir_addrs[src_dir]
            self.sync()

    def reset(self):
        """Load the directory to fileserver mappings from file and add to 
        the directory address map.
        """
        self.dir_addrs.clear()

        f = open(self.config_fname)
        lineno = 0
        for line in f:
            lineno += 1
            line = line.strip()
            if len(line) == 0 or line.startswith('#'):
                continue
            alist = line.split(' ',2)
            if len(alist) != 2:
                print 'Line',lineno, 'Must have format: path fsaddr'
                continue
            src_dir = alist[0].strip()
            if not src_dir.startswith('/'):
                print 'Line',lineno, 'srcdir must begin with a slash'
                continue
            host_port = alist[1].strip().split(':',1)
            if len(host_port) != 2:
                print 'Line',lineno, 'server addr must have format name:host'
                continue
            addr = (host_port[0], int(host_port[1]))
            self.dir_addrs[src_dir] = addr
        f.close()

    def sync(self):
        """Rewrite (sync) the config file with the current mappings.
        We comment out entries that we find in the file but not in the mapping.
        This can happen when someone does an rpc delete mapping call or someone
        has edited the file while server was running.
        """

        buf = StringIO.StringIO()

        # try and preserve comments, whitespace, user edits
        lineno = 0
        f = open(self.config_fname)
        for old_line in f:
            lineno += 1
            line = old_line.strip()
            if len(line) == 0 or line.startswith('#'):
                buf.write(old_line)
                continue
            alist = line.split(' ',2)
            if len(alist) != 2:
                print 'Line',lineno, 'Must have format: path fsaddr'
                buf.write(old_line)
                continue
            src_dir = alist[0].strip()
            if not src_dir.startswith('/'):
                print 'Line',lineno, 'srcdir must begin with a slash'
                buf.write(old_line)
                continue
            host_port = alist[1].strip().split(':',1)
            if len(host_port) != 2:
                print 'Line',lineno, 'server addr must have format name:host'
                buf.write(old_line)
                continue

            addr = (host_port[0], int(host_port[1]))

            if src_dir not in config:
                # must have been rpc deleted - comment out in case of mistake
                buf.write('# RPC deleted\n')
                buf.write('# ' + old_line)

        f.close()

        for src_dir, addr in config.iteritems():
            buf.write('%s %s\n' %(src_dir, addr2str(addr)))

        f = open(config_fname, 'w')
        f.write(buf.getvalue())
        f.close()

# raised if a lock fails
class LockError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
     return repr(self.value)

#
# this hold the lock state for every file
#
class LockState:

    def __init__(self, fid):
        self.fid = fid
        self.lock_type = LOCK_NONE
        self.holders = []

    def get_fid(self):
        return self.fid

    def get_count(self):
        return len(self.holders)

    def get_holders(self):
        return self.holders

    def update(self, lock_type, tid):

        if lock_type == LOCK_NONE:
            # release lock
            self.holders.remove(tid)
            if len(self.holders) == 0:
                self.lock_type = LOCK_NONE
            return True
        elif lock_type == LOCK_WATCH:
            if self.lock_type == LOCK_NONE or self.lock_type == LOCK_WATCH:
                self.holders.append(tid)
                self.lock_type = LOCK_WATCH
                return True
            elif len(self.holders) == 1 and self.holders[0] == tid:
                # allow single holder to change lock type
                self.lock_type = LOCK_WATCH
                return True
        elif lock_type == LOCK_READ:
            if self.lock_type == LOCK_WRITE or self.lock_type == LOCK_WATCH:
                # allow single holder to change lock type
                if len(self.holders) == 1 and self.holders[0] == tid:
                    self.lock_type = LOCK_READ
                    return True
            else:
                # lock is READ or NONE so can update
                self.holders.append(tid)
                self.lock_type = LOCK_READ
                return True
        elif lock_type == LOCK_WRITE:
            if self.lock_type == LOCK_NONE:
                self.holders.append(tid)
                self.lock_type = LOCK_WRITE
                return True
            elif len(self.holders) == 1 and self.holders[0] == tid:
                # allow single holder to change lock type
                self.lock_type = LOCK_WRITE
                return True
        else:
            raise LockError('Unknown lock type %d' % lock_type)

        return False

# 
# This implements the rpc server side of the directory/lock server
#
class ServerSocket(MessageSocket):

    def __init__(self, sock, addr,  dir_map):
        MessageSocket.__init__(self, sock, addr)

        self.dir_map = dir_map
        self.client_trans = set()

        print 'Client', self.addr, 'connected'

    def handle_closed(self):
        print 'Client', self.addr, 'disconnected'

        # connection gone, release all locks
        for tid in self.client_trans:
            try:
                print 'Release trans', tid
                release_trans(tid)
            except KeyError:
                pass
        self.client_trans.clear()

    # our directory inteface
    #
    # find closest match
    # We iterate in reverse order over the given pathname
    # begining with the pathname itself and the all of its
    # directory prefixs stopping if we find a match or hit root
    #
    # We sanitize the path name to git rid of multiple slashes
    # We remove the matched prefix from the search path
    #
    def lookup(self, name):
        addr = None
        clean_name = sanitize(name)
        dirname = clean_name
        while dirname:
            print 'try dirname', dirname
            addr = self.dir_map.lookup(dirname)
            if addr: 
                # found match
                break
            # try parent directory (prefix)
            if dirname == '/':
                # we hit root stop searching
                dirname = None
            else:
                dirname = get_dirname(dirname)
        if not addr:
            return (errno.ENOENT, '')
        # map name to servers directory
        server  = addr
        new_name = clean_name[len(dirname):]
        new_name = sanitize(new_name)

        print 'Mapping', name, 'to', new_name, 'on', server
        return (0, (server,new_name))

    #
    # Our locking interface
    #

    # lock a file id for a given transaction id
    def trans_lock(self, lock_type, fid, tid):

        try:
            # first fetch lock state for given object id
            lock = file_locks.get(fid, None)
            if not lock:
                lock = LockState(fid)
                file_locks[fid] = lock

            # not try locking it
            if not lock.update(lock_type, tid):
                # TODO add support for waiting
                raise LockError('cannot setlock')

            # update lock set for given transaction id
            lock_set = file_trans.get(tid, None)
            if not lock_set:
                lock_set = set()
                file_trans[tid] = lock_set
            lock_set.add(lock)

            # record for clean up
            self.client_trans.add(tid)
            
        except LockError, e:
            return (errno.EACCES, fid)
        except KeyError:
            return (errno.ENOENT, 0)

        return (0, '')

    # release all locks for transaction identifier
    def trans_unlock(self, tid):

        try:
            release_trans(tid)
            self.client_trans.remove(tid)
        except KeyError:
            return (errno.ENOENT, 0)

        return (0, '')

    #
    # This our admin interface
    #

    # return list of current mappings
    def get_diraddrs(self):
        return (0,self.dir_map.dump())

    # add a new mapping
    def add_diraddr(self, dname, fsaddr):
        self.dir_map.add(dname, fsaddr)
        return (0,'')

    # remove a mapping
    def del_diraddr(self, dname):
        self.dir_map.remove(dname)
        return (0,'')

#
# This implements the rpc client interface to the dir/lock server
#
class DirSocket(MessageSocket):

    def __init__(self, addr):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect(addr)

        MessageSocket.__init__(self, sock, addr)

    def lookup(self,dirname):
        return self.call('lookup', dirname)

    def trans_lock(self, lock_type, fid, tid):
        return self.call('trans_lock', lock_type, fid, tid)

    def trans_unlock(self, tid):
        return self.call('trans_unlock', tid)

    def get_diraddrs(self):
        return self.call('get_diraddrs')

    def add_diraddr(self, src_dir, fsaddr):
        return self.call('add_diraddr', src_dir, fsaddr)

    def del_diraddr(self, src_dir):
        return self.call('del_diraddr', src_dir)

if __name__ == '__main__':
    import sys,socket
    USAGE = "Usage: dirserver.py addr port setting-file"
    if len(sys.argv) == 4:
        # user override
        saddr = (sys.argv[1], int(sys.argv[2]))
        config_fname = sys.argv[3]
    else:
        if len(sys.argv) != 1:
            print USAGE
            sys.exit(1)
        # use defaults
        saddr = DFS_DIRADDR
        config_fname = DFS_DIRCONFIG

    dir_map = DirAddrMap(config_fname)
    print dir_map

    ss = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    ss.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)

    ss.bind(saddr)
    ss.listen(128)

    print 'Listening on', saddr

    run = True

    while run:
        try:
            cs,caddr = ss.accept()
            client = ServerSocket(cs, caddr, dir_map)
            client.start()
        except KeyboardInterrupt:
            run = False

    sys.exit(0)
